﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using MetaCollection.Framework.Model;

namespace MetaCollection.Framework.Change
{
	/// <summary>
	/// A change on an IItemInstance.  A change is computed by comparing to IItemInstances and listing the differences
	/// between the two as ItemChange instances.  These instances are stored in an ItemChangeCollection.  
	/// </summary>
	/// <remarks>Item changes are created through the ItemChangeCollection</remarks>
	public class ItemChange : IItemChange
	{
		/// <summary>
		/// Property associated with this change
		/// </summary>
		private PropertyInfo m_Property;
		private string m_PropertyKey;
		/// <summary>
		/// Optional parent property that the m_Property is assigned to within an IItemInstance.
		/// </summary>
		private PropertyInfo m_ParentProp;
		/// <summary>
		/// Key in the indexed parent property.
		/// </summary>
		private string m_ParentPropKey;

		/// <summary>
		/// The previous value for this change.
		/// </summary>
		private IItemInstance m_OriginalItem;
		/// <summary>
		/// The object this item change belongs to.  If we revert a change we will do it
		/// through assigning the m_OldValue to this object using m_Property.
		/// </summary>
		private IItemInstance m_ChangedItem;
		/// <summary>
		/// Property name
		/// </summary>
		private string m_PropertyName;
		/// <summary>
		/// The type of change this ItemChange represents.
		/// </summary>
		private ItemChangeTypes m_ChangeType = ItemChangeTypes.None;

		/// <summary>
		/// Initializes a new instance of the <see cref="ItemChange"/> class.
		/// </summary>
		/// <param name="changedItem">The changed item.</param>
		/// <param name="sourceItem">The source item.</param>
		/// <param name="prop">The property associated with this change.</param>
		/// <param name="addtionalPropertyInfo">The addtional property info. (optionally a key for the prop or a parent property info and key)</param>
		/// <remarks>For non-indexed </remarks>
		internal ItemChange(IItemInstance changedItem, IItemInstance sourceItem, PropertyInfo prop, params object[] addtionalPropertyInfo)
		{
			m_Property = prop;
			m_OriginalItem = sourceItem;
			m_ChangedItem = changedItem;
			

			if ((addtionalPropertyInfo.Count() != 0) && (addtionalPropertyInfo.Count() != 1) && (addtionalPropertyInfo.Count() != 2))
				throw new ArgumentException("Invalid arguments supplied for parent property.", "addtionalPropertyInfo");
			
			if (addtionalPropertyInfo.Count() == 2)
			{
				m_ParentProp = addtionalPropertyInfo[0] as PropertyInfo;
				m_ParentPropKey = addtionalPropertyInfo[1] as string;
				m_PropertyName = String.Format(CultureInfo.CurrentCulture, "IItemInstance.{0}[{1}].{2}", m_ParentProp.Name, m_ParentPropKey, prop.Name);
			}
			else if (addtionalPropertyInfo.Count() == 1)
			{
				m_PropertyKey = addtionalPropertyInfo[0] as string;
				m_PropertyName = String.Format(CultureInfo.CurrentCulture, "IItemInstance.{0}[{1}]", m_Property.Name, m_PropertyKey);
			}
			else
				m_PropertyName = String.Format(CultureInfo.CurrentCulture, "IItemInstance.{0}", prop.Name);

			m_ChangeType = m_ChangeType.UpdateChangeType(m_PropertyName, PreviousValue, CurrentValue);
		}


		#region IItemChange Members

		/// <summary>
		/// Gets the changes.
		/// </summary>
		/// <value>a list of itemchanges associated with this change.</value>
		public IList<IItemChange> Changes
		{
			get { return new[] { this }; }
		}

		/// <summary>
		/// Gets the type(s) of the change.
		/// </summary>
		/// <value>The type(s) of the change.</value>
		public ItemChangeTypes ChangeType
		{
			get { return m_ChangeType; }
		}

		/// <summary>
		/// Reverts this change.
		/// </summary>
		public void Revert()
		{
			// determine the source to use for the property by
			// trying a collection based parent, the current property as a collection parent and finally the item itself.
			object source = TryGetCollectionValue(m_ChangedItem, m_ParentProp, m_ParentPropKey) ?? TryGetCollectionValue(m_ChangedItem, m_Property, m_PropertyKey) ?? m_ChangedItem;
			if (source != null)
				m_Property.SetValue(source, PreviousValue, null);
		}

		/// <summary>
		/// Gets the friendly name of the property associated with this change.
		/// </summary>
		/// <value>The name of the property.</value>
		public string PropertyName
		{
			get { return m_PropertyName; }
		}

		/// <summary>
		/// Gets the current value.
		/// </summary>
		/// <value>The current value.</value>
		public object CurrentValue
		{
			get
			{
				return GetPropertyValue(m_ChangedItem);
			}
		}

		/// <summary>
		/// Gets the previous value.
		/// </summary>
		/// <value>The previous value.</value>
		public object PreviousValue
		{
			get
			{
				return GetPropertyValue(m_OriginalItem);
			}
		}

		/// <summary>
		/// Gets the description.
		/// </summary>
		/// <value>The description of this item change.</value>
		public string Description
		{
			get
			{
				if (m_ChangeType != ItemChangeTypes.None)
					return String.Format(CultureInfo.CurrentCulture, "{0} Change Made to {1} from \"{2}\" to \"{3}\"", m_ChangeType, PropertyName, PreviousValue, CurrentValue);
				else
					return "No Changes Made to " + PropertyName;
			}
		}

		/// <summary>
		/// Applies this change to a given item instance
		/// </summary>
		/// <param name="item">the instance to apply the change to</param>
		public void ApplyTo(IItemInstance item)
		{
			object source = TryGetCollectionValue(item, m_ParentProp, m_ParentPropKey) ?? TryGetCollectionValue(item, m_Property, m_PropertyKey) ?? item;
			
            
            if (source != null)
				m_Property.SetValue(source, CurrentValue, null);
			
            
            // TODO: work on this is required.  when setting the value on the collection we need to make a temporary array, change the value and replace the item enumerable with the updated temporary array.
		
            // all revert values code that works on the collections is broken until these changes are made.
		}

		public object GetChangeSource(IItemInstance item)
		{
			return  TryGetCollectionValue(item, m_ParentProp, m_ParentPropKey) ?? TryGetCollectionValue(item, m_Property, m_PropertyKey) ?? item;			 
		}

		/// <summary>
		/// Gets the property value (if the Parent Property is set then this will look for the property value under the parent property value).
		/// </summary>
		/// <param name="item">The item to get the property value from.</param>
		/// <returns>the value of the property.</returns>
		private object GetPropertyValue(IItemInstance item)
		{
			object source = TryGetCollectionValue(item, m_Property, m_PropertyKey);
			if (source != null)
				return source;

			source = TryGetCollectionValue(item, m_ParentProp, m_ParentPropKey) ?? item;
			if (source != null)
				return m_Property.GetValue(source, null);
			else
				return null;
		}

		/// <summary>
		/// If the Parent Property is set then this method will pull the instance of the property in the collection
		/// identified by the Parent Property Key.
		/// </summary>
		/// <param name="item">The item.</param>
		/// <returns></returns>
		private static object TryGetCollectionValue(IItemInstance item, PropertyInfo prop, string key)
		{
			// break out if there is no parent prop (a collection) or the item is null
			if (item == null || prop == null || String.IsNullOrEmpty(key))
				return null;

			if (prop.Name.Equals("Properties"))
				return (prop.GetValue(item, null) as IEnumerable<IItemProperty>).Where((i) => i.KeyName == key).FirstOrDefault();
			else if (prop.Name.Equals("AccessClaims"))
				return (prop.GetValue(item, null) as IEnumerable<IItemClaim>).Where((i) => string.Format(CultureInfo.CurrentCulture, "{0}-{1}-{2}", i.Id, i.ClaimType, i.ClaimValue) == key).FirstOrDefault();
			else if (prop.Name.Equals("Relationships"))
				return (prop.GetValue(item, null) as IEnumerable<IItemRelationship>).Where((i) => string.Format(CultureInfo.CurrentCulture, "{0}-{1}", i.RelationshipType.ToString(), i.TargetItem.ToString()) == key).FirstOrDefault();
			else
				throw new ArgumentException("An unsupported property was encountered - ." + prop.Name);
		}
		#endregion

		/// <summary>
		/// Returns a <see cref="T:System.String"/> that represents the current <see cref="T:System.Object"/>.
		/// </summary>
		/// <returns>
		/// A <see cref="T:System.String"/> that represents the current <see cref="T:System.Object"/>.
		/// </returns>
		public override string ToString()
		{
			return Description;
		}
	}
}
